﻿//******************************************************************************************************
//Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this
//software and associated documentation files (the "Software"), to deal in the Software
//without restriction, including without limitation the rights to use, copy, modify,
//merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
//permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all copies
//or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
//FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
//ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//*****************************************************************************************************

module internal SharpGeo.Core.DBase

open SharpGeo.Core
open ByteConversion

open System
open System.IO
open System.Text
open System.Data

type fieldType =
    | Unknown = 0
    | AutoIncrement = 1
    | Timestamp = 2
    | Binary = 3
    | Character = 4
    | Date = 5
    | Float = 6
    | OleData = 7
    | Long = 8
    | Logical = 9
    | Memo = 10
    | Numeric = 11
    | Double = 12

type fieldDescriptor = {FieldName:string; FieldType:fieldType; FieldLength:int; DecimalCount:int}

let getFieldType x =
    match x with
    | '+'B -> fieldType.AutoIncrement
    | '@'B -> fieldType.Timestamp
    | 'B'B -> fieldType.Binary
    | 'C'B -> fieldType.Character
    | 'D'B -> fieldType.Date
    | 'F'B -> fieldType.Float
    | 'G'B -> fieldType.OleData
    | 'I'B -> fieldType.Long
    | 'L'B -> fieldType.Logical
    | 'M'B -> fieldType.Binary
    | 'N'B -> fieldType.Numeric
    | 'O'B -> fieldType.Double
    | _ -> fieldType.Unknown

let getString x =
    Encoding.ASCII.GetString(x).Trim()

let convertToDate x =
    let str = getString x
    if str.Length < 8  || String.IsNullOrWhiteSpace(str) || str = "00000000" then
        box DBNull.Value
    else
        box (new DateTime(int str.[..3] + 1990, int str.[4..5], int str.[6..]))

let convertToDateTime x =
    let milliPerDay = 24L*60L*60L*1000L
    let millisSince4713 = -210866803200000L
    let days = int64 <| toLittleEndianInt32 x 0
    let time = int64 <| toLittleEndianInt32 x 4
    box (new DateTime((days*milliPerDay) + millisSince4713 + time))

let convertToDouble x =
    let str = getString x
    if str.[0] = '*' then
        box DBNull.Value
    else
        box (double str)

let convertToBool x =
    let str = (getString x).ToUpperInvariant()
    match str with
    | "Y" | "T" -> box true
    | "N" | "F" -> box false
    | _ -> box DBNull.Value

let convertData ft x =
    match ft with
    | fieldType.AutoIncrement | fieldType.Long -> box (toLittleEndianInt32 x)
    | fieldType.Date -> box (convertToDate x)
    | fieldType.Numeric | fieldType.Float -> box (convertToDouble x)
    | fieldType.Logical -> box (convertToBool x)
    | fieldType.Timestamp -> box (convertToDateTime x)
    | fieldType.Double -> box (toLittleEndianDouble x 0 )
    | fieldType.Character -> box (getString x)
    | _ -> box DBNull.Value

let getType ft =
    match ft with
    | fieldType.AutoIncrement | fieldType.Long -> typeof<int>
    | fieldType.Date | fieldType.Timestamp -> typeof<DateTime>
    | fieldType.Float | fieldType.Numeric | fieldType.Double -> typeof<double>
    | fieldType.Logical -> typeof<bool>
    | fieldType.Character -> typeof<string>
    | _ -> typeof<obj>

let getNullIndex (value:byte[]) index length =
    Array.tryFindIndex (fun x -> x = 0x00uy) value.[index..(index+length-1)]
    |> function
        | Some(x) -> x
        | None -> length

let readBlock (stream:Stream) size =
    let block = Array.zeroCreate size
    if stream.Read(block,0,size) <> size then
        failwith "unexpected file error"
    else
        block

//loads the 'raw' dbase data
let loadDBaseData (stream:Stream) =
    if stream = null || not stream.CanRead || not stream.CanSeek then invalidArg "stream" "stream cannot be null, non readable or non seekable"

    let header = readBlock stream 32

    //check the version
    if (int header.[0] &&& 0x07) <> 3 then failwith "invalid file"

    let numberOfRows = ByteConversion.toLittleEndianInt32 header 4

    //let languageID = header.[29]
    //let lastUpdated = new DateTime (int header.[1] + 1990, int header.[2], int header.[3])

    let fieldDescriptors =
        [|
        let fieldDescriptorsLeft = ref true
        while !fieldDescriptorsLeft do

            let fieldDescriptor = readBlock stream 32

            yield {
                FieldName = Encoding.ASCII.GetString(fieldDescriptor, 0, (getNullIndex fieldDescriptor 0 11)).Trim();
                FieldType = getFieldType fieldDescriptor.[11];
                FieldLength = int fieldDescriptor.[16];
                DecimalCount = int fieldDescriptor.[17];
                }

            if stream.ReadByte() = 0x0D then fieldDescriptorsLeft:= false
            else stream.Seek(-1L, SeekOrigin.Current) |> ignore
        |]

    let data = [|
        for i = 0 to numberOfRows-1 do
            yield [|
                stream.ReadByte() |> ignore //ignore the row status byte
                for field in fieldDescriptors do
                    yield convertData field.FieldType (readBlock stream field.FieldLength)
            |]
        |]
    (fieldDescriptors, data)

//adapts the 'raw' dbase data to a DataTable
let loadDBase stream name =
    let fields, data = loadDBaseData stream
    let dataTable = new DataTable(name)

    dataTable.BeginLoadData()

    for field in fields do
        dataTable.Columns.Add(field.FieldName, getType field.FieldType) |> ignore

    for row in data do
        dataTable.Rows.Add(row) |> ignore

    dataTable.EndLoadData()
    dataTable